package edu.umb.cs.antmanager.client;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.PrintStream;
import java.net.UnknownHostException;
import java.rmi.AccessException;
import java.rmi.AlreadyBoundException;
import java.rmi.NotBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.UnicastRemoteObject;
import java.util.ArrayList;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import com.healthmarketscience.rmiio.RemoteInputStream;
import com.healthmarketscience.rmiio.SimpleRemoteInputStream;

import edu.umb.cs.antmanager.common.AgentInterface;
import edu.umb.cs.antmanager.common.MachineStatus;
import edu.umb.cs.antmanager.common.ManagerInterface;
import edu.umb.cs.antmanager.domain.TestType;
import static edu.umb.cs.antmanager.common.Constants.*;

/**
 * Main class of the agent, implements <code>AgentInterface</code> it's
 * singleton, use static method <code>getRMIAgentServer()</code> to start one or
 * get the reference for current running agent instance.
 * 
 * @author 2009CS682-3UMB_AntManager
 * 
 */
public class RMIAgentServer extends UnicastRemoteObject implements
		AgentInterface {

	private static final long serialVersionUID = 1L;
	private ReadWriteLock lock;
	private static RMIAgentServer server;
	private MachineStatus machineStatus;
	private ManagerInterface managerStub;
	private File tmpFile;

	/**
	 * Create a agent server, if it exists just return the exist one
	 * 
	 * @return Agent Server
	 * @throws RemoteException
	 */
	public static synchronized RMIAgentServer getRMIAgentServer()
			throws RemoteException {
		if (server == null)
			server = new RMIAgentServer();
		return server;
	}

	private RMIAgentServer() throws RemoteException {
		super(AGENT_PORT);
		try {
			// machineStatus store status information about the agent
			machineStatus = new MachineStatus(java.net.InetAddress
					.getLocalHost().getHostName());
			System.setProperty("java.rmi.server.hostname", machineStatus
					.getMachineName());
		} catch (UnknownHostException e) {
			// host name unavailable, the agent can't operate correctly without
			// it
			System.err.println("cannot get agent's host name");
			System.exit(0);
		}
		// the agent is set to be idle at the beginning
		machineStatus.setStatus(STATUS_IDLE);
		lock = new ReentrantReadWriteLock();
	}

	/**
	 * Create <tt>Registry</tt> if it's not exist and bind agent to the
	 * registry.
	 */
	public static void startServer() {
		RMIRegistryInitializer.init();
		try {
			// get the stub for the registry, needed for binding
			Registry registry = LocateRegistry.getRegistry(AGENT_PORT);
			// create the agent server
			RMIAgentServer agent = RMIAgentServer.getRMIAgentServer();
			// bind the agent server to the registry as the name
			// AGENT_SERVER_NAME
			// the manager will use this name to find the agent in agent's
			// registry
			registry.bind(AGENT_SERVER_NAME, agent);
		} catch (AccessException e) {
			// the Registry is denied the agent's binding request
			System.err.println("Cannot bind the agent to the registry");
			System.exit(0);
		} catch (RemoteException e) {
			// the Registry is not acessible
			System.err.println("The agent's registry is not avaialable");
			System.exit(0);
		} catch (AlreadyBoundException e) {
			// There is another agent running on this machine
			System.err
					.println("You cannot run more than one agents on the same machine!");
			System.err.println("Please shutdwon the running agent first!");
			System.err
					.println("This window will close automatically in 10 seconds.");
			try {
				Thread.sleep(10000);
			} catch (InterruptedException e1) {
				e1.printStackTrace();
			}
			System.exit(0);
		}
		startDispatcher();
		System.out.println("Ant Agent " + server.machineStatus.getMachineName()
				+ " starts successfully");
	}

	private static void startDispatcher() {
		String javaHome = System.getenv("JAVA_HOME");
		String cp = System.getProperty("java.class.path");
		ArrayList<String> argList = new ArrayList<String>();
		if (javaHome != null)
			argList.add(javaHome + "/bin/java");
		else
			argList.add("java");
		argList.add("-classpath");
		argList.add(cp);
		argList.add(AgentDispatcher.class.getName());
		try {
			new ProcessBuilder(argList).start();
		} catch (IOException e) {
			e.printStackTrace();
		}
	}

	/**
	 * Start the agent server
	 * 
	 * @param args
	 */
	public static void main(String[] args) {
		try {
			System.setProperty("java.rmi.server.hostname", java.net.InetAddress
					.getLocalHost().getHostName());
		} catch (UnknownHostException e) {
		}
		RMIAgentServer.startServer();
	}

	/**
	 * For AntRunner to call back after finished the test. It will change the
	 * Agent's status to Idle, and send the test result back to the manager.
	 * Note: this code is running in AntRunner's thread.
	 */
	protected void freeServer() {
		lock.writeLock().lock();
		try {
			// connect the manager
			File file = new File(ANT_MANAGER_HOME
					+ machineStatus.getRunningTest().getName() + ".xml");
			FileInputStream is = new FileInputStream(file);
			RemoteInputStream result = new SimpleRemoteInputStream(is).export();
			managerStub.addTestLog(result, machineStatus);
			// update agent's status
			machineStatus.setStatus(STATUS_IDLE);
			machineStatus.setRunningTest(null);
			tmpFile.delete();
		} catch (RemoteException e) {
			//
		} catch (FileNotFoundException e) {
			System.err.println("Cannot find "
					+ machineStatus.getRunningTest().getName() + ".xml");
		} finally {
			lock.writeLock().unlock();
		}
	}

	@Override
	public MachineStatus getStatus() throws RemoteException {
		lock.readLock().lock();
		try {
			return machineStatus;
		} finally {
			lock.readLock().unlock();
		}
	}

	@Override
	public boolean executeTask(TestType test, ManagerInterface manager)
			throws RemoteException {
		lock.writeLock().lock();
		// TODO should the agent check the test's resource requirement?
		try {
			if (machineStatus.getStatus().equals(STATUS_IDLE) && test != null) {
				try {
					tmpFile = File.createTempFile("AntAgent", "");
					new Thread(new AntRunner(test, this, new PrintStream(
							new FileOutputStream(tmpFile), true))).start();
					machineStatus.setStatus(STATUS_BUSY);
					machineStatus.setRunningTest(test);
					this.managerStub = manager;
					return true;
				} catch (IOException e) {
					e.printStackTrace();
				}
			}
			// The agent is busy
			return false;
		} finally {
			lock.writeLock().unlock();
		}
	}

	@Override
	public void shutDown() throws RemoteException {
		Registry registry = LocateRegistry.getRegistry(AGENT_PORT);
		try {
			registry.unbind(AGENT_SERVER_NAME);
		} catch (NotBoundException e) {
		}
		UnicastRemoteObject.unexportObject(server, true);
		server = null;
		new Thread(new Runnable() {
			public void run() {
				System.exit(0);
			}
		}).start();
	}

	@Override
	public void restart() throws RemoteException {
		Registry registry = LocateRegistry.getRegistry(AGENT_PORT);
		try {
			registry.unbind(AGENT_SERVER_NAME);
		} catch (NotBoundException e) {
		}
		UnicastRemoteObject.unexportObject(server, true);
		server = null;
		RMIAgentServer.startServer();
	}

	@Override
	public RemoteInputStream executeCommand(String cmd) throws RemoteException {
		RemoteInputStream ris = null;
		lock.writeLock().lock();
		try {
			// check if agent is idle
			if (!machineStatus.getStatus().equals(STATUS_IDLE))
				return null;
			try {
				machineStatus.setStatus(STATUS_BUSY);
				// Construct command parameters
				String javaHome = System.getenv("JAVA_HOME");
				String cp = System.getProperty("java.class.path");
				ArrayList<String> argList = new ArrayList<String>();
				if (javaHome != null)
					argList.add(javaHome + "/bin/java");
				else
					argList.add("java");
				argList.add("-classpath");
				argList.add(cp);
				argList.add("org.apache.tools.ant.launch.Launcher");
				for (String c : cmd.split(" "))
					argList.add(c);

				// Create process
				ProcessBuilder pb = new ProcessBuilder(argList);
				// redirect the output, so the caller can get the output
				pb.redirectErrorStream(true);
				pb.directory(new File(ANT_MANAGER_HOME));
				Process antProc = pb.start();
				new Thread(new AntProcessChecker(antProc)).start();
				// Export the output stream
				ris = new SimpleRemoteInputStream(antProc.getInputStream())
						.export();
			} catch (FileNotFoundException e) {
				e.printStackTrace();
			} catch (IOException e) {
				e.printStackTrace();
			}
		} finally {
			lock.writeLock().unlock();
		}
		return ris;
	}

	/**
	 * for command line task execution, as the code in Ant will call
	 * <code>System.exit()</code>, we have to run it in another process, as a
	 * result we lost control over it. this class with start a thread
	 * Periodically to check if the process finished. Once it finished it will
	 * set the agent's status to idle.
	 * 
	 * @author 2009CS682-3UMB_AntManager
	 * 
	 */
	private class AntProcessChecker implements Runnable {
		private Process proc;

		public AntProcessChecker(Process proc) {
			this.proc = proc;
		}

		@Override
		public void run() {
			boolean isDone = false;
			while (!isDone)
				try {
					Thread.sleep(1000);
					// check if the process exit
					proc.exitValue();
					isDone = true;
				} catch (IllegalThreadStateException e) {

				} catch (InterruptedException e) {

				}
			// process done, set Agent to idle
			lock.writeLock().lock();
			try {
				machineStatus.setStatus(STATUS_IDLE);
			} finally {
				lock.writeLock().unlock();
			}
		}

	}

	@Override
	public RemoteInputStream getRunningLog() throws RemoteException {
		if (machineStatus.getRunningTest() != null) {
			try {
				return new SimpleRemoteInputStream(new FileInputStream(tmpFile))
						.export();
			} catch (FileNotFoundException e) {
				e.printStackTrace();
			}
		}
		return null;
	}
}
